--  -------------------------------------------------------------------------
--  @File: comment_parser.adb
--
--  @Project: LiberAda
--  @Version: 1.0
--  @Created: 2009-10-19
--  @Author:  Markus Knau�, Julian Trischler
--
--  @Description:
--    For detailed description (@see: comment_parser.ads)
--  @Modifications:
--    +2009-11-17 MK: Initial release
--    +2011-07-14 JT: Added table of contents support
with Ada.Text_IO;
with Ada.Strings;
with Ada.Numerics.Discrete_Random;

with Settings;

package body Comment_Parser is

   --  ----------------------------------------------------------------------
   --  @Function: Start_Comment
   function Start_Comment return Comment_Type is
      New_Comment : Comment_Type;
   begin
      --  Actually no special initialization is needed.
      return New_Comment;
   end Start_Comment;

   --  ----------------------------------------------------------------------
   --  @Procedure: Add_Line
   procedure Add_Line (This : in out Comment_Type; Line : in String) is
   begin
      This.Add_Line (ASU.To_Unbounded_String (Line));
   end Add_Line;

   --  ----------------------------------------------------------------------
   --  @Procedure: Strip
   --  @Description:
   --    Removes all leading and trailing white space, and the comment
   --    indicators from a comment line. Divider lines are also removed.
   --  @Parameter:
   --    +Line: The comment line to strip
   procedure Strip (Line : in out ASU.Unbounded_String) is
      use ASU;
      use Ada.Strings;
   begin
      --  Remove all leading and trailing spaces
      Trim (Line, Both);

      --  Remove the comment indicators '--' and the now leading spaces
      if Length (Line) > 2 then
         if To_String (Head (Line, 2)) = "--" then
            Tail (Line, Length (Line) - 2);
            Trim (Line, Left);
         end if;
      elsif Length (Line) = 2 then
         --  If there are only comment indicators the line is cleared.
         Line := To_Unbounded_String ("");
      end if;

      --  If this is a divider line, clear it completely
      declare
         Has_Only_Hyphens : Boolean := True;
      begin
         for J in 1 .. Length (Line) loop
            if Element (Line, J) /= '-' then
               Has_Only_Hyphens := False;
               exit;
            end if;
         end loop;
         if Has_Only_Hyphens then
            Line := To_Unbounded_String ("");
         end if;
      end;
   end Strip;

   --  ----------------------------------------------------------------------
   --  @Function: Is_New_Paragraph
   function Is_New_Paragraph
     (Line : in ASU.Unbounded_String) return Boolean is
      use ASU;
   begin
      if Length (Line) > 1 then
         if Element (Line, 1) = '@' then
            --  If @see: is at the beginning of a line it does not
            --  denote a paragraph.
            if Length (Line) > 4 and then
               Slice (Line, 2, 4) = "see" then
               return False;
            end if;

            return Index (Line, ":") > 0;
         end if;
      end if;
      return False;
   end Is_New_Paragraph;

   --  ----------------------------------------------------------------------
   --  @Function: Parse_Paragraph_Type
   function Parse_Paragraph_Type
     (Line : in ASU.Unbounded_String) return Paragraph_Type is
      use ASU;
      use Ada.Strings;
   begin
      if Is_New_Paragraph (Line) then
         if Length (Line) > 1 and then Element (Line, 1) = '@' then
            declare
               Type_Identifier : Unbounded_String :=
                 Tail (Line, Length (Line) - 1);
            begin
               if Index (Type_Identifier, ":") > 0 then
                  Head (Type_Identifier, Index (Type_Identifier, ":") - 1);
                  for J in Paragraph_Identifier_Mappings'Range loop
                     if Type_Identifier =
                       Paragraph_Identifier_Mappings (J).Identifier then
                        return Paragraph_Identifier_Mappings (J).Par_Type;
                     end if;
                  end loop;
               end if;
            end;
         end if;
      end if;
      return UNKNOWN_T;
   end Parse_Paragraph_Type;

   --  ----------------------------------------------------------------------
   --  @Procedure: Remove_Paragraph_Indicator
   procedure Remove_Paragraph_Indicator
     (Line : in out ASU.Unbounded_String) is
      use ASU;
      use Ada.Strings;
   begin
      if Index (Line, "@") = 1 and then Index (Line, ":") > 1 then
         Tail (Line, Length (Line) - Index (Line, ":"));
         Trim (Line, Left);
      end if;
   end Remove_Paragraph_Indicator;

   --  ----------------------------------------------------------------------
   --  @Function: Is_List_Item_Start
   function Is_List_Item_Start
     (Comment : in ASU.Unbounded_String) return Boolean is
   begin
      if ASU.Length (Comment) > 1 then
         return ASU.Element (Comment, 1) = '+' and
           ASU.Index (Comment, ":") > 1;
      end if;
      return False;
   end Is_List_Item_Start;

   --  ----------------------------------------------------------------------
   --  @Function: Get_List_Label
   function Get_List_Label
     (Comment : in ASU.Unbounded_String) return ASU.Unbounded_String is
   begin
      if Is_List_Item_Start (Comment) then
         return ASU.Unbounded_Slice (Comment, 2, ASU.Index (Comment, ":") - 1);
      end if;
      return ASU.Null_Unbounded_String;
   end Get_List_Label;

   --  ----------------------------------------------------------------------
   --  @Function: Strip_List_Label
   function Strip_List_Label
     (Comment : in ASU.Unbounded_String) return ASU.Unbounded_String is
      use ASU;
      use Ada.Strings;
   begin
      if Is_List_Item_Start (Comment) then
         return Trim (Unbounded_Slice (
           Comment, Index (Comment, ":") + 1, Length (Comment)), Both);
      end if;
      return Null_Unbounded_String;
   end Strip_List_Label;

   --  ----------------------------------------------------------------------
   --  @Procedure: Add_Line
   procedure Add_Line
     (This : in out Comment_Type;
      Line : in     ASU.Unbounded_String) is

      Comment : ASU.Unbounded_String := Line;
   begin
      Strip (Comment);
      if Is_New_Paragraph (Comment) then
         --  This is a new paragraph
         declare
            Paragraph : Paragraph_Content_Type;
         begin
            Paragraph.Par_Type := Parse_Paragraph_Type (Comment);
            Remove_Paragraph_Indicator (Comment);
            if ASU.Length (Comment) > 0 then
               declare
                  Clause : Clause_Type;
               begin
                  ASU.Append (Clause.Sentence, Comment);
                  Paragraph.Clauses.Append (Clause);
               end;
            end if;
            This.Paragraphs.Append (Paragraph);
         end;
      else
         --  TODO:
         --  OR This is a new list
         --  OR This is a new clause in a list
         --  TODO:
         --  Parse the @see references
         if not This.Paragraphs.Is_Empty then
         --  Only comments in paragraphs are added. If there is no
         --  paragraph, the comment is not added.
            declare
               Paragraph : Paragraph_Content_Type :=
               This.Paragraphs.Last_Element;
               New_Clause : Clause_Type;
            begin
               --  If this comment is empty, this is a new line
               --  indicator. A new clause starts.
               if ASU.Length (Comment) <= 0 then
                  ASU.Append (
                    New_Clause.Sentence, ASU.To_Unbounded_String (""));
                  Paragraph.Clauses.Append (New_Clause);
               else
                  if Is_List_Item_Start (Comment) then
                     declare
                        Clause : Clause_Type (List);
                     begin
                        Clause.Label    := Get_List_Label (Comment);
                        Clause.Sentence := Strip_List_Label (Comment);
                        Paragraph.Clauses.Append (Clause);
                     end;
                  else
                     declare
                        Clause : Clause_Type;
                     begin
                        if Paragraph.Clauses.Is_Empty then
                           Clause.Sentence := Comment;
                        else
                           Clause := Paragraph.Clauses.Last_Element;
                           Paragraph.Clauses.Delete_Last;
                           ASU.Append (Clause.Sentence, " ");
                           ASU.Append (Clause.Sentence, Comment);
                        end if;
                        Paragraph.Clauses.Append (Clause);
                     end;
                  end if;
               end if;
               This.Paragraphs.Delete_Last;
               This.Paragraphs.Append (Paragraph);
            end;
         end if;
      end if;
   end Add_Line;

   --  ----------------------------------------------------------------------
   --  @Procedure: Print_Clause
   procedure Print_Clause (Clause_Crsr : in Clause_Vectors.Cursor) is
      Clause : constant Clause_Type := Clause_Vectors.Element (Clause_Crsr);
   begin
      if Clause.Sentence_Type = Simple then
         Ada.Text_IO.Put_Line ("  Clause " &
           "[" & Sentence_Types'Image (Clause.Sentence_Type) & "]: " &
           ASU.To_String (Clause.Sentence));
      elsif Clause.Sentence_Type = List then
         Ada.Text_IO.Put_Line ("  Clause " &
           "[" & Sentence_Types'Image (Clause.Sentence_Type) & "] " &
           ASU.To_String (Clause.Label) & ": " &
           ASU.To_String (Clause.Sentence));
      end if;
   end Print_Clause;

   --  ----------------------------------------------------------------------
   --  @Procedure: Print_Paragraph
   procedure Print_Paragraph (Paragraph_Crsr : in Paragraph_Vectors.Cursor) is
      Paragraph : constant Paragraph_Content_Type :=
        Paragraph_Vectors.Element (Paragraph_Crsr);
   begin
      Ada.Text_IO.Put_Line ("Type: " &
        Paragraph_Type'Image (Paragraph.Par_Type));
      if not Paragraph.Clauses.Is_Empty then
         Paragraph.Clauses.Iterate (Print_Clause'Access);
      end if;
   end Print_Paragraph;

   --  ----------------------------------------------------------------------
   --  @Procedure: Finish_Comment
   procedure Finish_Comment (This : in out Comment_Type) is
   begin
      --  Actually there isn't anything to do to finish a comment. This
      --  procedure is simply a place holder for future extensions.
      if Settings.Verbose then
         Ada.Text_IO.New_Line;
         Ada.Text_IO.New_Line;
         Ada.Text_IO.Put_Line ("--  Comment Parser ------------------------");
         if not This.Paragraphs.Is_Empty then
            This.Paragraphs.Iterate (Print_Paragraph'Access);
            Ada.Text_IO.New_Line;
            Ada.Text_IO.New_Line;
         end if;
      end if;
   end Finish_Comment;

   --  ----------------------------------------------------------------------
   --  @Function: "="
   function "=" (Left, Right : in Clause_Type) return Boolean is
      use type ASU.Unbounded_String;
   begin
      if Left.Sentence_Type = Right.Sentence_Type then
         if Left.Sentence /= Right.Sentence then
            return False;
         end if;
         case Left.Sentence_Type is
            when List =>
               return Left.Label = Right.Label;
            when others =>
               null;
         end case;
      end if;
      return False;
   end "=";

   --  ----------------------------------------------------------------------
   --  @Function: Get_Paragraph_Type
   function Get_Paragraph_Type
     (This : in Comment_Type) return Paragraph_Type is
      Par : Paragraph_Content_Type;
   begin
      --  The type of this whole comment is stored in its first paragraph.
      if not This.Paragraphs.Is_Empty then
         Par := Paragraph_Vectors.Element (This.Paragraphs.First);
         return Par.Par_Type;
      end if;
      return UNKNOWN_T;
   end Get_Paragraph_Type;

   --  ----------------------------------------------------------------------
   --  @Function: Get_Paragraph_Identifier
   function Get_Paragraph_Identifier
     (Par_Type : in Paragraph_Type) return String is
   begin
      for J in Paragraph_Identifier_Mappings'Range loop
         if Paragraph_Identifier_Mappings (J).Par_Type = Par_Type then
            return ASU.To_String (
              Paragraph_Identifier_Mappings (J).Identifier);
         end if;
      end loop;
      return "UNKNOWN";
   end Get_Paragraph_Identifier;

   subtype Random_Range is Integer range 0 .. 100000;
   package Random_Integers is new Ada.Numerics.Discrete_Random (Random_Range);
   Generator : Random_Integers.Generator;

   --  ----------------------------------------------------------------------
   --  @Procedure: Random
   --  @Description: Returns a random number in 0 .. 100.000 as a string.
   --  @Purpose: Used to generate unique hyper targets
   --  @Return: random string
   function Random return String is
      Rand : constant String := Random_Integers.Random (Generator)'Img;
   begin
      return Rand (Rand'First + 1 .. Rand'Last);
   end Random;

   --  ----------------------------------------------------------------------
   --  @Procedure: Add_To_Document
   procedure Add_To_Document
     (This : in     Comment_Type;
      Doc  : in out Part.Part_Type) is

      Par_Crsr    : Paragraph_Vectors.Cursor;
      Clause_Crsr : Clause_Vectors.Cursor;

      Comment_Part : Part.Part_Type;
      Par          : Paragraph_Content_Type;
      Par_Part     : Part.Part_Type;
      Clause       : Clause_Type;
      Clause_Part  : Part.Part_Type;
      List_Part    : Part.Part_Type;
      In_List      : Boolean := False;
   begin
      if not This.Paragraphs.Is_Empty then
         Comment_Part := Part.Create;
         if Part.Has_Anchor (Comment_Part) then
            Part.Add_Attribute_Before (Comment_Part, "<div class='box' id='" &
              Part.Anchor (Comment_Part) & "'>");

            Part.Add_Attribute_After (Comment_Part, "</div>");
         end if;
         Part.Set_Anchor (Comment_Part, Get_Title (This) & "-" & Random);
         Part.Set_Title (Comment_Part, Get_Title (This), 3);
         Part.Add_Part (Doc, Comment_Part);

         Par_Crsr := This.Paragraphs.First;
         while Paragraph_Vectors.Has_Element (Par_Crsr) loop
            Par      := Paragraph_Vectors.Element (Par_Crsr);
            Par_Part := Part.Create;
            Part.Add_Attribute_Before (Par_Part,
              "<b>");
            Part.Set_Value (
              Par_Part, Get_Paragraph_Identifier (Par.Par_Type) & ": ");
            Part.Add_Attribute_After (Par_Part, "</b>");
            Part.Add_Part (Comment_Part, Par_Part);

            if not Par.Clauses.Is_Empty then
               Clause_Crsr := Par.Clauses.First;
               while Clause_Vectors.Has_Element (Clause_Crsr) loop
                  Clause := Clause_Vectors.Element (Clause_Crsr);

                  if Clause.Sentence_Type = List then
                     --  Add a new entry to an existing list
                     if not In_List then
                        --  Start a new list
                        List_Part := Part.Create;
                        In_List   := True;
                        Part.Add_Attribute_Before (List_Part, "<ul>");
                        Part.Add_Attribute_After (List_Part, "</ul>");
                        Part.Add_Part (Comment_Part, List_Part);
                     end if;

                     Clause_Part := Part.Create;
                     Part.Add_Attribute_Before (Clause_Part, "<li><b>");
                     Part.Add_Attribute_After (Clause_Part, "</b>");
                     Part.Set_Value (
                       Clause_Part, ASU.To_String (Clause.Label) & ": ");
                     Part.Add_Part (List_Part, Clause_Part);
                     Clause_Part := Part.Create;
                     Part.Add_Attribute_After (Clause_Part, "</li>");
                     Part.Set_Value (
                       Clause_Part, ASU.To_String (Clause.Sentence));
                     Part.Add_Part (List_Part, Clause_Part);
                  else
                     --  Add a simple entry to a paragraph
                     if In_List then
                        --  We were in a list and finish it now
                        In_List := False;
                     end if;
                     Clause_Part := Part.Create;
                     Part.Set_Value (
                       Clause_Part, ASU.To_String (Clause.Sentence));
                     Part.Add_Attribute_After (Clause_Part, "<br>");
                     Part.Add_Part (Comment_Part, Clause_Part);
                  end if;

                  Clause_Crsr := Clause_Vectors.Next (Clause_Crsr);
               end loop;
            end if;

            Par_Crsr := Paragraph_Vectors.Next (Par_Crsr);
         end loop;
      end if;
   end Add_To_Document;

   --  ----------------------------------------------------------------------
   --  @Function: Get_Title
   function Get_Title (This : in Comment_Type) return String is
      Par : Paragraph_Content_Type;
   begin
      if not This.Paragraphs.Is_Empty then
         Par := Paragraph_Vectors.Element (This.Paragraphs.First);
         if not Par.Clauses.Is_Empty then
            return ASU.To_String (
              Clause_Vectors.Element (Par.Clauses.First).Sentence);
         end if;
      end if;
      return "";
   end Get_Title;

end Comment_Parser;
